package bu.met.cs664.cox;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static bu.met.cs664.cox.Board.Player.*;
import static bu.met.cs664.cox.Board.BoardState.*;

/**
 * Begins a new game of Mancala. Maintains game state, prompts for next moves, and outputs results.
 *
 * @author mcox
 */
public class Mancala {

    /** Type of player is either human or computer */
    public static enum PlayerType {
        HUMAN('h'),
        COMPUTER('c');

        /** type code for the player type */
        private char typeCode;

        /** Build a new player type with the associated code */
        private PlayerType(char typeCode) {
            this.typeCode = typeCode;
        }

        /** Return the type code */
        public char getTypeCode() {
            return this.typeCode;
        }

        /** Utility to look up a player type by code */
        public static PlayerType getByTypeCode(char typeCode) {
            for (PlayerType playerType : PlayerType.values()) {
                if (playerType.getTypeCode() == typeCode) {
                    return playerType;
                }
            }

            throw new IllegalArgumentException("Unknown type code " + typeCode);
        }
    }

    /** Computer strength as a function of its evaluation depth */
    public static enum ComputerStrength {
        UNSET(' ', 0),
        BEGINNER('b', 1), // 6 evaluations
        NORMAL('n', 3), // 216 evaluations
        HARD('h', 5), // 7776 evaluations
        EXTREME('e', 7), // 279936 evaluations
        IMPOSSIBLE('i', 9); // 10077696 evaluations

        /** char identifying a strength */
        private char typeCode;

        /** Return the type code */
        public char getTypeCode() {
            return this.typeCode;
        }

        /** How deep will the computer evaluate */
        private int depth;

        /** Build a new computer strength with associated code and depth */
        private ComputerStrength(char typeCode, int depth) {
            this.typeCode  = typeCode;
            this.depth = depth;
        }

        /** Get the depth of the computer */
        public int getDepth() {
            return depth;
        }

        /** Utility to look up a player type by code */
        public static ComputerStrength getByTypeCode(char typeCode) {
            for (ComputerStrength computerStrength : ComputerStrength.values()) {
                if (computerStrength.getTypeCode() == typeCode) {
                    return computerStrength;
                }
            }

            throw new IllegalArgumentException("Unknown type code " + typeCode);
        }
    }

    /**
     * Start a new game. Initialize the game board and players. Allows for players to be either human or computer
     * controlled. Prompts each player for their next move and outputs the results.
     */
    public static void main (String [] args) {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))) {
            System.out.println("///////////////////////////////////////");
            System.out.println("Starting new game of Mancala -- Welcome");
            System.out.println("///////////////////////////////////////\n\n");

            List<Character> playerChoices = new ArrayList<>();
            playerChoices.add('h');
            playerChoices.add('c');

            List<Character> strengthChoices = new ArrayList<>();
            strengthChoices.add('b');
            strengthChoices.add('n');
            strengthChoices.add('h');
            strengthChoices.add('e');
            strengthChoices.add('i');

            List<Character> turnChoices = new ArrayList<>();
            turnChoices.add('1');
            turnChoices.add('2');
            turnChoices.add('3');
            turnChoices.add('4');
            turnChoices.add('5');
            turnChoices.add('6');

            PlayerType playerOneType = PlayerType.getByTypeCode(
                    promptClient("Player One is (h)uman or (c)omputer?", playerChoices, reader));
            ComputerStrength computerOneStrength = ComputerStrength.UNSET;

            if (playerOneType == PlayerType.COMPUTER) {
                computerOneStrength = ComputerStrength.getByTypeCode(
                        promptClient("Computer strength is (b)eginner, (n)ormal, (h)ard, (e)xtreme, or (i)mpossible",
                                strengthChoices, reader));
            }

            PlayerType playerTwoType = PlayerType.getByTypeCode(
                    promptClient("Player two is (h)uman or (c)omputer?", playerChoices, reader));
            ComputerStrength computerTwoStrength = ComputerStrength.UNSET;

            if (playerTwoType == PlayerType.COMPUTER) {
                computerTwoStrength = ComputerStrength.getByTypeCode(
                        promptClient("Computer strength is (b)eginner, (n)ormal, (h)ard, (e)xtreme, or (i)mpossible",
                                strengthChoices, reader));
            }

            Board board = new Board();
            Map<Board.Player, PlayerType> playerToType = new HashMap<>();
            playerToType.put(PLAYER_ONE, playerOneType);
            playerToType.put(PLAYER_TWO, playerTwoType);
            Map<Board.Player, ComputerStrength> playerToStrength = new HashMap();
            playerToStrength.put(PLAYER_ONE, computerOneStrength);
            playerToStrength.put(PLAYER_TWO, computerTwoStrength);

            Board.Player currentPlayer = PLAYER_ONE;
            System.out.println("\n=========== Starting game ===========\n");

            while (true) {
                int pit;
                if (playerToType.get(currentPlayer) == PlayerType.HUMAN) {
                    pit = promptTurn(String.format("Player %s choose a pit to move", currentPlayer), turnChoices, board,
                            currentPlayer, reader);
                } else {
                    pit = MiniMax.calculateNextMove(board, currentPlayer, Board.Player.complement(currentPlayer),
                            playerToStrength.get(currentPlayer).getDepth());
                }

                System.out.println(String.format("%s moves %d", currentPlayer, pit + 1));
                Board.TurnResult result = board.takeTurn(currentPlayer, pit);
                System.out.println(String.format("Turn result: player captured %d stones, opponent captured %d " +
                        "stones, player one has captured %d stones total, player two has captured %d stones total",
                        result.getStonesCaptured(), result.getStonesOpponentCaptured(),
                        result.getPlayerOneStore(), result.getPlayerTwoStore()));

                if (currentPlayer == PLAYER_ONE) {
                    System.out.println(board.renderForP1());
                } else {
                    System.out.println(board.renderForP2());
                }

                if (result.isGameOver()) {
                    System.out.println("Game over with a result " + result.getState());
                    break;
                } else if (result.getState() != SAME_PLAYER) {
                    currentPlayer = Board.Player.complement(currentPlayer);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            System.exit(1);
        }
    }

    /** Utility to prompt the client to initialize players with either human or computer. */
    private static char promptClient(String message, List<Character> validInputs, BufferedReader reader) throws IOException {
        char result = ' ';

        while (result == ' ') {
            System.out.println(message);
            String input = reader.readLine();

            if (input != null && input.length() == 1 && validInputs.contains(input.toLowerCase().charAt(0))) {
                result = input.toLowerCase().charAt(0);
            } else {
                System.out.println("Invalid entry");
            }
        }

        return result;
    }

    /** Utility to prompt human clients to enter their next move. */
    private static int promptTurn(String message, List<Character> validInputs, Board board, Board.Player player,
                                  BufferedReader reader) throws IOException {
        int result = -1;
        String error = "You must enter in a number from 1-6 corresponding to your pits 1-6";

        while (result == -1) {
            System.out.println(message);
            String input = reader.readLine();

            if (input != null && input.length() == 1 && validInputs.contains(input.charAt(0))) {
                try {
                    result = Integer.parseInt(input.substring(0,1)) - 1; // normalize to zero-index
                } catch (NumberFormatException e) {
                    System.out.println(error);
                }

                if (!board.isValidMove(player, result)) {
                    System.out.println(String.format("You cannot move from pit %d as it has zero stones in it.", result + 1));
                    result = -1;
                }
            } else {
                System.out.println(error);
            }
        }

        return result;
    }
}
